﻿namespace Cellfish.Toolbox.Facts.Streams
{
    using System;
    using System.IO;
    using System.Threading;
    using System.Threading.Tasks;

    using Cellfish.Test;
    using Cellfish.Test.Fakes.System;
    using Cellfish.Test.Fakes.System.IO;
    using Cellfish.Toolbox.Streams;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class PassThruStreamFacts
    {
        private FakeStream fakeStream;

        private PassThruStream stream;

        [TestInitialize]
        public void Setup()
        {
            this.fakeStream = new FakeStream();
            this.stream = new PassThruStream(this.fakeStream);
        }

        [TestMethod]
        public void Given_null_stream_When_creating_PassThruStream_Then_ArgumentNullException_is_thrown()
        {
            Ensure.Throws<ArgumentNullException>(() => new PassThruStream(null));
        }

        [TestMethod]
        public void Given_PassThruStream_When_setting_properties_Then_wrapped_stream_is_updated()
        {
            this.stream.Position = 42;
            Assert.AreEqual(42, this.fakeStream.Position, "Failed to set Position.");
            this.stream.ReadTimeout = 47;
            Assert.AreEqual(47, this.fakeStream.ReadTimeout, "Failed to set ReadTimeout.");
            this.stream.WriteTimeout = 11;
            Assert.AreEqual(11, this.fakeStream.WriteTimeout, "Failed to set WriteTimeout.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_getting_properties_Then_values_from_wrapped_stream_are_returned()
        {
            this.fakeStream.Position = 42;
            Assert.AreEqual(42, this.stream.Position, "Failed to get Position."); 
            this.fakeStream.ReadTimeout = 47;
            Assert.AreEqual(47, this.stream.ReadTimeout, "Failed to get ReadTimeout.");
            this.fakeStream.WriteTimeout = 11;
            Assert.AreEqual(11, this.stream.WriteTimeout, "Failed to get WriteTimeout.");
            this.fakeStream.Behaviors.CanRead = true;
            Assert.IsTrue(this.stream.CanRead, "Failed to get CanRead.");
            this.fakeStream.Behaviors.CanSeek = true;
            Assert.IsTrue(this.stream.CanSeek, "Failed to get CanSeek.");
            this.fakeStream.Behaviors.CanTimeout = true;
            Assert.IsTrue(this.stream.CanTimeout, "Failed to get CanTimeout.");
            this.fakeStream.Behaviors.CanWrite = true;
            Assert.IsTrue(this.stream.CanWrite, "Failed to get CanWrite.");
            this.fakeStream.Behaviors.Length = 17;
            Assert.AreEqual(17L, this.stream.Length, "Failed to get Length.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_flushing_Then_flush_of_wrapped_stream_is_called()
        {
            bool flushCalled = false;
            this.fakeStream.Behaviors.Flush = () => { flushCalled = true; };
            this.stream.Flush();
            Assert.IsTrue(flushCalled);
        }

        [TestMethod]
        public void Given_PassThruStream_When_flushing_async_Then_flush_async_of_wrapped_stream_is_called()
        {
            bool flushCalled = false;
            this.fakeStream.Behaviors.FlushAsync = _ =>
            {
                flushCalled = true;
                return Task.FromResult(true);
            };
            Assert.IsTrue(this.stream.FlushAsync(new CancellationToken()).Wait(TimeSpan.FromSeconds(15)), "FlushAsync did not complete in a timely manner.");
            Assert.IsTrue(flushCalled);
        }

        [TestMethod]
        public void Given_PassThruStream_When_seeking_Then_seek_of_wrapped_stream_is_called()
        {
            long newPosition = 47;
            SeekOrigin setOrigion = SeekOrigin.Begin;
            this.fakeStream.Behaviors.Seek = (pos, origin) =>
                {
                    newPosition = pos;
                    setOrigion = origin;
                    return 11;
                };
            Assert.AreEqual(11L, this.stream.Seek(47, SeekOrigin.Current), "Seek returned the wrong value.");
            Assert.AreEqual(47L, newPosition, "Set position is wrong.");
            Assert.AreEqual(SeekOrigin.Current, setOrigion, "Set origin is wrong.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_setting_length_Then_set_length_of_wrapped_stream_is_called()
        {
            long newLength = 0;
            this.fakeStream.Behaviors.SetLength = length => { newLength = length; };
            this.stream.SetLength(42);
            Assert.AreEqual(42L, newLength, "Set length is wrong.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_reading_synchronously_Then_read_of_wrapped_stream_is_called()
        {
            var beforeObserver = new FakeObserver<PassThruStream.BeforeData>
                                     {
                                         OnError = _ => { },
                                         OnNext = d =>
                                             {
                                                 Assert.AreEqual(
                                                     47,
                                                     d.Offset,
                                                     "Before have wrong offset");
                                                 Assert.AreEqual(
                                                     11,
                                                     d.Count,
                                                     "Before have wrong count");
                                             }
                                     };
            var afterObserver = new FakeObserver<int>
                                    {
                                        OnError = _ => { },
                                        OnNext = d => Assert.AreEqual(1, d, "After have wrong count")
                                    };
            int readBytes = 0;
            int readOffset = 0;
            this.fakeStream.Behaviors.Read = (buf, off, length) =>
                {
                    buf[0] = 17;
                    readOffset = off;
                    readBytes = length;
                    return 1;
                };
            Assert.AreEqual(17, this.stream.ReadByte(), "ReadByte returned the wrong value.");
            Assert.AreEqual(0, readOffset, "ReadByte used wrong offset.");
            Assert.AreEqual(1, readBytes, "ReadByte used wrong length.");
            this.stream = new PassThruStream(this.fakeStream);
            Assert.AreEqual(1, this.stream.Read(new byte[1024], 47, 11), "Read(..., 47, 11) returned wrong length.");
            Assert.AreEqual(47, readOffset, "Read(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, readBytes, "Read(..., 47, 11) used wrong length.");
            
        }

        [TestMethod]
        public void Given_PassThruStream_When_reading_asynchronously_Then_read_async_of_wrapped_stream_is_called()
        {
            int readBytes = 0;
            int readOffset = 0;
            this.fakeStream.Behaviors.ReadAsync = (buf, off, length, cancellation) =>
            {
                buf[0] = 17;
                readOffset = off;
                readBytes = length;
                return Task.FromResult(42);
            };
            Assert.AreEqual(42, this.stream.ReadAsync(new byte[1024], 47, 11).Result, "ReadAsync(..., 47, 11) returned wrong length.");
            Assert.AreEqual(47, readOffset, "ReadAsync(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, readBytes, "ReadAsync(..., 47, 11) used wrong length.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_reading_asynchronously_using_APM_Then_read_async_of_wrapped_stream_is_called()
        {
            int readBytes = 0;
            int readOffset = 0;
            this.fakeStream.Behaviors.ReadAsync = (buf, off, length, cancellation) =>
            {
                buf[0] = 17;
                readOffset = off;
                readBytes = length;
                return Task.FromResult(42);
            };
            bool callbackCalled = false;
            var result = this.stream.BeginRead(new byte[1024], 47, 11, _ => { callbackCalled = true; }, "MyState");
            Assert.AreEqual("MyState", result.AsyncState.ToString(), "AsyncState is wrong.");
            Assert.AreEqual(42, this.stream.EndRead(result), "EndRead returned wrong length.");
            Assert.AreEqual(47, readOffset, "BeginRead(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, readBytes, "BeginWrite(..., 47, 11) used wrong length.");
            Assert.IsTrue(callbackCalled, "APM callback was not called.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_writing_synchronously_Then_write_of_wrapped_stream_is_called()
        {
            var beforeObserver = new FakeObserver<PassThruStream.BeforeData>
                                     {
                                         OnError = _ => { },
                                         OnNext = d =>
                                             {
                                                 Assert.AreEqual(
                                                     47,
                                                     d.Offset,
                                                     "Before have wrong offset");
                                                 Assert.AreEqual(
                                                     11,
                                                     d.Count,
                                                     "Before have wrong count");
                                             }
                                     };
            var afterObserver = new FakeObserver<int>
                                    {
                                        OnError = _ => { },
                                        OnNext = d => Assert.AreEqual(11, d, "After have wrong count")
                                    };
            int writeBytes = 0;
            int writeOffset = 0;
            byte writtenByte = 0;
            this.fakeStream.Behaviors.Write = (buf, off, length) =>
            {
                writtenByte = buf[0];
                writeOffset = off;
                writeBytes = length;
            };
            this.stream.WriteByte(17);
            Assert.AreEqual((byte)17, writtenByte, "WriteByte wrote the wrong value.");
            Assert.AreEqual(0, writeOffset, "WriteByte used wrong offset.");
            Assert.AreEqual(1, writeBytes, "WriteByte used wrong length.");
            this.stream = new PassThruStream(this.fakeStream, beforeWrite: beforeObserver, afterWrite: afterObserver);
            this.stream.Write(new byte[1024], 47, 11);
            Assert.AreEqual(47, writeOffset, "Write(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, writeBytes, "Write(..., 47, 11) used wrong length.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_writing_asynchronously_Then_write_async_of_wrapped_stream_is_called()
        {
            int writeBytes = 0;
            int writeOffset = 0;
            byte writtenByte = 0;
            this.fakeStream.Behaviors.WriteAsync = (buf, off, length, cancellation) =>
            {
                writtenByte = buf[0];
                writeOffset = off;
                writeBytes = length;
                return Task.FromResult(true);
            };
            this.stream.WriteAsync(new byte[1024], 47, 11).Wait();
            Assert.AreEqual(47, writeOffset, "Write(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, writeBytes, "Write(..., 47, 11) used wrong length.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_writing_asynchronously_using_APM_Then_write_async_of_wrapped_stream_is_called()
        {
            int writeBytes = 0;
            int writeOffset = 0;
            byte writtenByte = 0;
            this.fakeStream.Behaviors.WriteAsync = (buf, off, length, cancellation) =>
            {
                writtenByte = buf[0];
                writeOffset = off;
                writeBytes = length;
                return Task.FromResult(true);
            };
            bool callbackCalled = false;
            var result = this.stream.BeginWrite(new byte[1024], 47, 11, _ => { callbackCalled = true; }, "MyState");
            Assert.AreEqual("MyState", result.AsyncState.ToString(), "AsyncState is wrong.");
            this.stream.EndWrite(result);
            Assert.AreEqual(47, writeOffset, "BeginWrite(..., 47, 11) used wrong offset.");
            Assert.AreEqual(11, writeBytes, "BeginWrite(..., 47, 11) used wrong length.");
            Assert.IsTrue(callbackCalled, "APM callback was not called.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_closing_Then_wrapped_stream_is_closed()
        {
            this.stream.Close();
            Assert.IsTrue(this.fakeStream.Behaviors.Disposed);
        }

        [TestMethod]
        public void Given_PassThruStream_When_disposing_Then_wrapped_stream_is_disposed()
        {
            this.stream.Dispose();
            Assert.IsTrue(this.fakeStream.Behaviors.Disposed);
        }

        [TestMethod]
        public void Given_PassThruStream_When_reading_and_wrapped_stream_throws_ArgumentException_Then_before_observer_sees_error()
        {
            this.fakeStream.Behaviors.Read = (buf, o, c) => { throw new ArgumentException(); };
            this.fakeStream.Behaviors.ReadAsync = (buf, o, c, cancelletaion) => { throw new ArgumentException(); };
            bool onErrorCalled = false;
            var beforeObserver = new FakeObserver<PassThruStream.BeforeData>
                                     {
                                         OnCompleted = () => { },
                                         OnNext = _ => { },
                                         OnError = _ => onErrorCalled = true
                                     };
            this.stream = new PassThruStream(this.fakeStream, beforeRead: beforeObserver);
            Ensure.Throws<ArgumentException>(() => this.stream.ReadByte(), "Read did not throw argument exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for Read.");
            onErrorCalled = false;
            Ensure.Throws<ArgumentException>(() => this.stream.ReadAsync(new byte[1], 0, 1).Wait(), "ReadAsync did not throw argument exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for ReadAsync.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_writing_and_wrapped_stream_throws_ArgumentException_Then_before_observer_sees_error()
        {
            this.fakeStream.Behaviors.Write = (buf, o, c) => { throw new ArgumentException(); };
            this.fakeStream.Behaviors.WriteAsync = (buf, o, c, cancelletaion) => { throw new ArgumentException(); };
            bool onErrorCalled = false;
            var beforeObserver = new FakeObserver<PassThruStream.BeforeData>
            {
                OnCompleted = () => { },
                OnNext = _ => { },
                OnError = _ => onErrorCalled = true
            };
            this.stream = new PassThruStream(this.fakeStream, beforeWrite: beforeObserver);
            Ensure.Throws<ArgumentException>(() => this.stream.WriteByte(11), "Write did not throw argument exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for Write.");
            onErrorCalled = false;
            Ensure.Throws<ArgumentException>(() => this.stream.WriteAsync(new byte[1], 0, 1).Wait(), "WriteAsync did not throw argument exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for WriteAsync.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_reading_and_wrapped_stream_throws_Exception_Then_after_observer_sees_error()
        {
            this.fakeStream.Behaviors.Read = (buf, o, c) => { throw new Exception(); };
            this.fakeStream.Behaviors.ReadAsync = (buf, o, c, cancelletaion) => Task.Factory.StartNew<int>(() => { throw new Exception(); });
            bool onErrorCalled = false;
            var afterObserver = new FakeObserver<int>
            {
                OnCompleted = () => { },
                OnNext = _ => { },
                OnError = _ => onErrorCalled = true
            };
            this.stream = new PassThruStream(this.fakeStream, afterRead: afterObserver);
            Ensure.Throws<Exception>(() => this.stream.ReadByte(), "Read did not throw exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for Read.");
            onErrorCalled = false;
            Ensure.TaskFails<Exception>(this.stream.ReadAsync(new byte[1], 0, 1), "ReadAsync did not fail.");
            Assert.IsTrue(onErrorCalled, "OnError not called for ReadAsync.");
        }

        [TestMethod]
        public void Given_PassThruStream_When_writing_and_wrapped_stream_throws_Exception_Then_after_observer_sees_error()
        {
            this.fakeStream.Behaviors.Write = (buf, o, c) => { throw new Exception(); };
            this.fakeStream.Behaviors.WriteAsync = (buf, o, c, cancelletaion) => Task.Factory.StartNew(() => { throw new Exception(); });
            bool onErrorCalled = false;
            var afterObserver = new FakeObserver<int>
            {
                OnCompleted = () => { },
                OnNext = _ => { },
                OnError = _ => onErrorCalled = true
            };
            this.stream = new PassThruStream(this.fakeStream, afterWrite: afterObserver);
            Ensure.Throws<Exception>(() => this.stream.WriteByte(11), "Write did not throw exception.");
            Assert.IsTrue(onErrorCalled, "OnError not called for Write.");
            onErrorCalled = false;
            Ensure.TaskFails<Exception>(this.stream.WriteAsync(new byte[1], 0, 1), "WriteAsync did not fail.");
            Assert.IsTrue(onErrorCalled, "OnError not called for WriteAsync.");
        }
    }
}
